package it.unibo.refolding.pico;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.DoublePair;
import it.unibo.refolding.common.DoubleQuartet;
import it.unibo.refolding.common.DoubleTriad;
import it.unimi.dsi.fastutil.doubles.DoubleList;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.SortedSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;

public class RetractionSegment {
    public final double retractionVelocityMPerS;
    public final SortedSet<RetractionPeak> peaks;
    public final double noiseMeanN, noiseStddevN;

    public RetractionSegment(
        double retractionVelocityMPerS, Iterable<RetractionPeak> peaks, double noiseMeanN,
        double noiseStddevN) {
    Preconditions.checkArgument(
      !Double.isNaN(noiseMeanN) & !Double.isInfinite(noiseMeanN),
      "noiseMeanN=%s must be a finite number", noiseMeanN);
    Preconditions.checkArgument(
      noiseStddevN >= 0 & !Double.isInfinite(noiseStddevN),
      "noiseStddevN=%s must be a finite non-negative number", noiseStddevN);
    this.retractionVelocityMPerS = retractionVelocityMPerS;
    this.noiseMeanN = noiseMeanN;
    this.noiseStddevN = noiseStddevN;
    this.peaks = ImmutableSortedSet.<RetractionPeak>copyOf(
      RetractionPeak.CONTOUR_LENGTH_COMPARATOR, peaks);
    }

    // Supports both with and without noise statistics.
    public static final String segmentRegex = String.format(
        "RetractionSegment\\((%s), (%s)(?:, (%s), (%s))?\\)", CommonThings.FP_REGEX,
        "\\[[^]]*\\]", CommonThings.FP_REGEX, CommonThings.FP_REGEX);
    public static final Pattern segmentPattern = Pattern.compile(segmentRegex);
    
    public static final RetractionSegment EMPTY = new RetractionSegment(
        0, ImmutableList.<RetractionPeak>of(), 0, 0);

    public static RetractionSegment parseRepr(String s) {
        Matcher matcher = segmentPattern.matcher(s);
        boolean match = matcher.matches();
        Preconditions.checkArgument(match, "%s doesn't match RetractionSegment pattern.", s);
        double noiseMeanN, noiseStddevN;
        if (matcher.group(3) == null) {
            noiseMeanN = noiseStddevN = 0;
        } else {
            assert matcher.group(4) != null;
            noiseMeanN = Double.parseDouble(matcher.group(3));
            noiseStddevN = Double.parseDouble(matcher.group(4));
        }
        return new RetractionSegment(
            Double.parseDouble(matcher.group(1)), RetractionPeak.parseListRepr(matcher.group(2)), noiseMeanN,
            noiseStddevN);
    }

    List<DoublePair> contourLengthAndNonNegativeRuptureForceOfPeaksFromContactPointOnwards() {
        List<DoublePair> clRuptureForce = new ArrayList<>();
        for (RetractionPeak peak : peaks) {
            clRuptureForce.add(DoublePair.of(peak.contourLengthM, peak.ruptureForceN));
        }
        return clRuptureForce;
    }
    
    List<DoubleTriad> contourLengthNonNegativeRuptureForceAndExtensionOfPeaksFromContactPointOnwards() {
        List<DoubleTriad> clRuptureForce = new ArrayList<>();
        for (RetractionPeak peak : peaks) {
            clRuptureForce.add(DoubleTriad.of(peak.contourLengthM, peak.ruptureForceN, peak.extensionM));
        }
        return clRuptureForce;
    }
    
    List<DoubleQuartet> contourLengthNonNegativeRuptureForceExtensionAndDistanceOfPeaksFromContactPointOnwards(
        double springConstantNPerM) {
        List<DoubleQuartet> clRuptureForce = new ArrayList<>();
        for (RetractionPeak peak : peaks) {
            clRuptureForce.add(DoubleQuartet.of(
                peak.contourLengthM, peak.ruptureForceN, peak.extensionM,
                Math.max(0, peak.extensionM - peak.ruptureForceN / springConstantNPerM)));
        }
        return clRuptureForce;
    }

    @Override
    public String toString() {
        return repr();
    }

    public String repr() {
        /*
         * For a canonical representation of the value, use
         * Float.toString(float) or Double.toString(double) as appropriate.
         */
        return String.format(
            Locale.ROOT, "RetractionSegment(%s, %s, %s, %s)", Double.toString(retractionVelocityMPerS),
            RetractionPeak.repr(peaks), Double.toString(noiseMeanN), Double.toString(noiseStddevN));
    }
    
    public boolean isEmpty() {
        return peaks.isEmpty();
    }

    public RetractionPeak lastPeak() {
        return peaks.last();
    }

    public DoubleList peakContourLengthsFromContactPointOnwards() {
        return RetractionPeak.contourLengths(peaks);
    }
    
    @Override
    public int hashCode() {
        return Objects.hashCode(retractionVelocityMPerS, peaks);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof RetractionSegment)) {
            return false;
        }
        RetractionSegment other = (RetractionSegment) obj;
        return Double.doubleToLongBits(retractionVelocityMPerS) == Double
            .doubleToLongBits(other.retractionVelocityMPerS) & peaks.equals(other.peaks);
    }

    public static String repr(List<RetractionSegment> segments) {
        return '[' + StringUtils.join(segments, ", ") + ']';
    }

    public static List<RetractionSegment> parseListRepr(String s) {
        String[] s2 = CommonThings.parseArrayRepr(s, "RetractionSegment");
        RetractionSegment[] peaks = new RetractionSegment[s2.length];
        for (int i = peaks.length; --i >= 0;) {
            String r = (i == 0 ? "" : "RetractionSegment") + s2[i];
            peaks[i] = RetractionSegment.parseRepr(r);
        }
        return Collections.unmodifiableList(Arrays.asList(peaks));
    }
}
